﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;

namespace Ssdb.Provider
{
    /// <summary>
    ///     Class SsdbSocket.
    /// </summary>
    public class SsdbSocket
    {
        /// <summary>
        ///     The buffe r_ size
        /// </summary>
        private const int BUFFER_SIZE = 8 * 1024;

        /// <summary>
        ///     The recv buf
        /// </summary>
        private MemoryStream recvBuf = new MemoryStream(BUFFER_SIZE);

        /// <summary>
        ///     The socket
        /// </summary>
        // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
        private TcpClient socket;

        /// <summary>
        ///     Initializes a new instance of the <see cref="SsdbSocket" /> class.
        /// </summary>
        public SsdbSocket()
        {
            this.socket = new TcpClient("0.0.0.0", 8888) { NoDelay = true };
            this.socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="SsdbSocket" /> class.
        /// </summary>
        /// <param name="host">The host.</param>
        /// <param name="port">The port.</param>
        public SsdbSocket(string host, int port)
        {
            this.socket = new TcpClient(host, port) { NoDelay = true };
            this.socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="SsdbSocket" /> class.
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        public SsdbSocket(string connectionString = "SsdbConnectiongString")
        {
            var config = SsdbConfig.GetConfig(connectionString);
            this.socket = new TcpClient(config.HostName, config.Port) { NoDelay = true };
            this.socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
        }

        ~SsdbSocket()
        {
            this.Close();
        }

        public void Close()
        {
            this.socket?.Close();
            this.socket = null;
        }

        public List<byte[]> Request(string cmd, params string[] args)
        {
            List<byte[]> req = new List<byte[]>(1 + args.Length) { Encoding.Default.GetBytes(cmd) };
            req.AddRange(args.Select(s => Encoding.Default.GetBytes(s)));
            return this.Request(req);
        }

        public List<byte[]> Request(string cmd, params byte[][] args)
        {
            List<byte[]> req = new List<byte[]>(1 + args.Length) { Encoding.Default.GetBytes(cmd) };
            req.AddRange(args);
            return this.Request(req);
        }

        public List<byte[]> Request(IEnumerable<byte[]> req)
        {
            MemoryStream buf = new MemoryStream();
            foreach (byte[] p in req)
            {
                byte[] len = Encoding.Default.GetBytes(p.Length.ToString());
                buf.Write(len, 0, len.Length);
                buf.WriteByte((byte)'\n');
                buf.Write(p, 0, p.Length);
                buf.WriteByte((byte)'\n');
            }
            buf.WriteByte((byte)'\n');

            byte[] bs = buf.GetBuffer();
            this.socket.GetStream().Write(bs, 0, (int)buf.Length);
            //Console.Write(Encoding.Default.GetString(bs, 0, (int)buf.Length));
            return this.Recv();
        }

        private static int Memchr(byte[] bs, byte b, int offset)
        {
            for (int i = offset; i < bs.Length; i++)
            {
                if (bs[i] == b)
                {
                    return i;
                }
            }
            return -1;
        }

        private List<byte[]> Parse()
        {
            List<byte[]> list = new List<byte[]>();
            byte[] buf = this.recvBuf.GetBuffer();

            int idx = 0;
            while (true)
            {
                int pos = Memchr(buf, (byte)'\n', idx);
                //System.out.println("pos: " + pos + " idx: " + idx);
                if (pos == -1)
                {
                    break;
                }
                if (pos == idx || (pos == idx + 1 && buf[idx] == '\r'))
                {
                    idx += 1; // if '\r', next time will skip '\n'
                    // ignore empty leading lines
                    if (list.Count == 0)
                    {
                        continue;
                    }
                    int left = (int)this.recvBuf.Length - idx;
                    this.recvBuf = new MemoryStream(8192);
                    if (left > 0)
                    {
                        this.recvBuf.Write(buf, idx, left);
                    }
                    return list;
                }
                byte[] lens = new byte[pos - idx];
                Array.Copy(buf, idx, lens, 0, lens.Length);
                int len = int.Parse(Encoding.Default.GetString(lens));

                idx = pos + 1;
                if (idx + len >= this.recvBuf.Length)
                {
                    break;
                }
                byte[] data = new byte[len];
                Array.Copy(buf, idx, data, 0, data.Length);

                //Console.WriteLine("len: " + len + " data: " + Encoding.Default.GetString(data));
                idx += len + 1; // skip '\n'
                list.Add(data);
            }
            return null;
        }

        private List<byte[]> Recv()
        {
            while (true)
            {
                List<byte[]> ret = this.Parse();
                if (ret != null)
                {
                    return ret;
                }
                byte[] bs = new byte[8192];
                int len = this.socket.GetStream().Read(bs, 0, bs.Length);
                //Console.WriteLine("<< " + Encoding.Default.GetString(bs));
                this.recvBuf.Write(bs, 0, len);
            }
        }
    }

    /// <summary>
    ///     Class SsdbConfig.
    /// </summary>
    internal class SsdbConfig
    {
        /// <summary>
        ///     Gets or sets the name of the host.
        /// </summary>
        /// <value>The name of the host.</value>
        public string HostName { get; set; }

        /// <summary>
        ///     Gets or sets the port.
        /// </summary>
        /// <value>The port.</value>
        public int Port { get; set; }

        /// <summary>
        ///     Gets the configuration.
        /// </summary>
        /// <returns>SsdbConfig.</returns>
        public static SsdbConfig GetConfig(string connectionString = "SsdbConnectiongString")
        {
            return new SsdbConfig();
        }
    }
}